(provide "graph2")
(require "graphics")

;;;;
;;;;
;;;; Scatmat Object Prototype
;;;;
;;;;

(send scatmat-proto :title "Scatterplot Matrix")
(send scatmat-proto :menu-title "Scatmat")
(send scatmat-proto :fixed-aspect t)
(send scatmat-proto :menu-template (send graph-proto :menu-template))

(defmeth scatmat-proto :redraw-background () (send self :erase-window))


;;;;
;;;;
;;;; Spinner Object Prototype
;;;;
;;;;

(send spin-proto :title "Spinning Plot")
(send spin-proto :menu-title "Spinner")
(send spin-proto :fixed-aspect t)
(send spin-proto :menu-template (append (send graph-proto :menu-template)
                                        '(dash faster slower cuing axes)))
(send spin-proto :variable-labels '("X" "Y" "Z"))
(send spin-proto :black-on-white nil)
(send spin-proto :depth-cuing t)
(send spin-proto :showing-axes t)
(send spin-proto :scale-type 'variable :draw nil)
                                        
(defmeth spin-proto :isnew (&rest args)
  (apply #'call-next-method args)
  (send self :add-overlay (send spin-control-overlay-proto :new)))

(defmeth spin-proto :adjust-to-data (&key (draw t))
  (call-next-method :draw nil)
  (when (null (send self :scale-type))
        (let* ((vars (send self :num-variables))
               (ranges (send self :range (iseq 0 (- vars 1))))
               (radius (* (sqrt vars)
                          (max (- (min ranges)) (max ranges)))))
          (send self :center (iseq vars) 0 :draw nil)
          (send self :range (iseq vars) (- radius) radius :draw nil)))
  (when draw
        (send self :resize)
        (send self :redraw)))

(defmeth spin-proto :rotation-type (&optional (new nil set))
  (if set (setf (slot-value 'rotation-type) new))
  (slot-value 'rotation-type))

(defmeth spin-proto :make-menu-item (item)
  (if (symbolp item)
      (case item
        (faster (send spin-speed-item-proto :new self 1.5))
        (slower (send spin-speed-item-proto :new self (/ 2 3)))
        (cuing  (send graph-item-proto :new "Depth Cuing" self
                      :depth-cuing :depth-cuing :toggle t :redraw t))
        (axes   (send graph-item-proto :new "Show Axes" self
                      :showing-axes :showing-axes :toggle t :redraw t))
        (t (call-next-method item)))
      item))
      
(defmeth spin-proto :add-surface (x y z &key (draw t) (type 'solid) (spline 3)
				    color)
"Args: (x y z &key (draw t) (type 'solid) (spline 3) color)
Adds a grid surface using sequences X, Y with values in the matrix Z.
Z should be (length X) by (length Y)."
  (let ((z (row-list z)))
    (mapcar #'(lambda (u z) 
               (let* ((yz (if spline
                              (spline y z :xvals (* spline (length y)))
                              (list y z)))
                      (y (first yz))
                      (z (second yz)))
                 (send self
                       :add-lines 
                       (list (repeat u (length y)) y z) 
                       :draw nil :type type :color color)))
            x z))
  (let ((z (column-list z)))
    (mapcar #'(lambda (u z)
                (let* ((xz (if spline 
                               (spline x z :xvals (* spline (length x)))
                               (list x z)))
                       (x (first xz))
                       (z (second xz)))
                  (send self
                        :add-lines 
                        (list x (repeat u (length x)) z)
                        :draw nil :type type :color color)))
            y z))
  (if draw (send self :redraw))
  nil)
  
(defmeth spin-proto :add-function (f xmin xmax ymin ymax &rest args &key (num-points 6))
"Args: (f xmin xmax ymin ymax &rest args &key (num-points 6))
Adds surface of function F over a NUM-POINTS by NUM-POINTS grid on the
rectangle [xmin, xmax] x [ymin, ymax]. Passes other keywords to
:add-surface method."
  (let* ((x (rseq xmin xmax num-points))
         (y (rseq ymin ymax num-points))
         (z (outer-product x y f)))
    (apply #'send self :add-surface x y z args)))
  
(defmeth spin-proto :abcplane (a b c &rest args)
"Message args: (a b c)
Adds the graph of the plane A + B x + Cy to the plot."
  (let ((xlimits (send self :range 0))
        (ylimits (send self :range 1)))
    (apply #'send self :add-function #'(lambda (x y) (+ a (* b x) (* c y)))
	   (- (mean xlimits) (/ (abs (apply #'- xlimits)) (* 2 (sqrt 3))))
	   (+ (mean xlimits) (/ (abs (apply #'- xlimits)) (* 2 (sqrt 3))))
	   (- (mean ylimits) (/ (abs (apply #'- ylimits)) (* 2 (sqrt 3))))
	   (+ (mean ylimits) (/ (abs (apply #'- ylimits)) (* 2 (sqrt 3))))
	   :spline nil
	   args)))

;;
;; Spinner control overlay
;;

(defproto spin-control-overlay-proto 
          '(top lefts gap side ascent box-top text-base)
          ()
          graph-overlay-proto)

(defmeth spin-control-overlay-proto :isnew ()
  (setf (slot-value 'gap) 5)
  (setf (slot-value 'side) 10)
  (setf (slot-value 'ascent) (send graph-proto :text-ascent))
  (let ((w1 (send graph-proto :text-width "Pitch"))
        (w2 (send graph-proto :text-width "Roll"))
        (w3 (send graph-proto :text-width "Yaw"))
        (gap (slot-value 'gap))
        (side (slot-value 'side)))
    (setf (slot-value 'lefts)
          (list (* 2 gap)
                (+ (* 3 gap) side)
                (+ (* 6 gap) (* 2 side) w1)
                (+ (* 7 gap) (* 3 side) w1)
                (+ (* 11 gap) (* 4 side) w1 w2)
                (+ (* 12 gap) (* 5 side) w1 w2)))))

(defmeth spin-control-overlay-proto :resize ()
  (let* ((graph (send self :graph))
         (height (send graph :canvas-height))
         (bottom-margin (fourth (send graph :margin)))
         (top (+ (- height bottom-margin) 1))
         (gap (slot-value 'gap))
         (side (slot-value 'side))
         (ascent (send graph :text-ascent))
         (text-base (+ top gap (max side ascent)))
         (box-top (- text-base side)))
    (setf (slot-value 'top) top)
    (setf (slot-value 'text-base) text-base)
    (setf (slot-value 'box-top) box-top)))

(defmeth spin-control-overlay-proto :redraw ()
  (let ((graph (slot-value 'graph))
        (top (slot-value 'top))
        (lefts (slot-value 'lefts))
        (gap (slot-value 'gap))
        (side (slot-value 'side))
        (text-base (slot-value 'text-base))
        (box-top (slot-value 'box-top)))
    (send graph :draw-line 0 top (send graph :canvas-width) top)
    (mapcar #'(lambda (x) (send graph :frame-rect x box-top side side))
            lefts)
    (mapcar #'(lambda (s x y) (send graph :draw-string s x y))
            '("Pitch" "Roll" "Yaw")
            (+ (select lefts '(1 3 5)) gap side) 
            (repeat text-base 3))))

(defmeth spin-control-overlay-proto :do-click (x y m1 m2)
  (declare (ignore m2))
  (let ((graph (slot-value 'graph))
        (top (slot-value 'top))
        (lefts (slot-value 'lefts))
        (gap (slot-value 'gap))
        (side (slot-value 'side))
        (text-base (slot-value 'text-base))
        (box-top (slot-value 'box-top)))
    (when (< top y)
          (send graph :idle-on nil)
          (if (< box-top y text-base)
              (let ((i (car (which (< lefts x (+ lefts side)))))
                    (angle (abs (send graph :angle))))
                (when i
                      (send graph :rotation-type 
                            (select '(pitching rolling yawing)
                                    (floor (/ i 2))))
                      (send graph :angle (if (oddp i) angle (- angle)))
                      (send graph :while-button-down
                            #'(lambda (x y) (send graph :rotate))
                            nil)
                      (send graph :idle-on m1))))
          t)))


;;
;; Spinner Menu Items
;;

;; SPIN-SPEED-ITEM-PROTO. multiply speed by fixed number to speed up or slow down. 
(defproto spin-speed-item-proto '(graph mult) () menu-item-proto)

(defmeth spin-speed-item-proto :isnew (v m)
  (setf (slot-value 'graph) v)
  (setf (slot-value 'mult) m)
  (call-next-method (if (> 1 m) "Slower" "Faster"))
  (send self :key (if (> 1 m) #\S #\F)))
  
(defmeth spin-speed-item-proto :do-action ()
  (send (slot-value 'graph) :angle (* (slot-value 'mult) (send (slot-value 'graph) :angle))))


;;;;
;;;;
;;;; Spinner Functions
;;;;
;;;;

(defun spin-function (f xmin xmax ymin ymax &rest args)
"Args: (f xmin xmax ymin ymax &key (num-points 6) (spline 3))
Rotatable plot of function F of two real variables over the range
between [xmin, xmax] x [ymin, ymax]. The function is evaluated at
NUM-POINTS points. If SPLINE is not NIL a spline is fit at 
(* SPLINE NUMPOINTS) points."
  (let ((plot (apply #'send spin-proto :new 3 :show nil args)))
    (apply #'send plot :add-function f xmin xmax ymin ymax :draw nil args)
    (send plot :adjust-to-data :draw nil)
    (send plot :new-menu)
    (send plot :showing-axes nil)
    (send plot :rotate-2 0 1 (/ pi 3) :draw nil)
    (send plot :rotate-2 1 2 (- (/ pi 3)) :draw nil)
    (send plot :show-window)
    plot))

;;;;
;;;;
;;;; Name List Object Prototype
;;;;
;;;;

(send name-list-proto :title "Name List")
(send name-list-proto :menu-title "List")
(send name-list-proto :menu-template '(link mouse dash erase-selection 
                                            focus-on-selection show-all
					    color
                                            selection dash options
#+unix                                      save-image))

(defmeth name-list-proto :clear-content ()
    (apply #'send self :erase-rect (send self :view-rect)))


;;;;
;;;;
;;;; Histogram Object Prototype
;;;;
;;;;

(send histogram-proto :title "Histogram")
(send histogram-proto :menu-title "Histogram")
(send histogram-proto :fixed-aspect nil)
(send histogram-proto :size 250 125)
(send histogram-proto :menu-template '(link mouse resize-brush dash 
                                            erase-selection
                                            focus-on-selection show-all
					    color
                                            selection dash 
					    slicer 
                                            rescale 
					    options 
#+unix                                      save-image
                                            dash change-bins))

(defmeth histogram-proto :make-menu-item (item)
  (if (symbolp item)
      (case item
        (change-bins (send change-hist-bins-item-proto :new self))
        (t (call-next-method item)))
      item))
      
(defmeth histogram-proto :drag-point (x y &key (draw t))
  (let ((p (call-next-method x y :draw nil)))
    (if p (send self :resize))
    (if (and p draw) (send self :redraw))
    p))

;;
;; Histogram Menu Items
;;

;; CHANGE-HIST-BINS-ITEM-PROTO. Opens new integer dialog.
(defproto change-hist-bins-item-proto '(graph) () menu-item-proto)

(defmeth change-hist-bins-item-proto :isnew (h)
  (setf (slot-value 'graph) h)
  (call-next-method "Change Bins"))

(defmeth change-hist-bins-item-proto :do-action ()
  (let ((bins (get-new-integer "Number of bins"
                               2
                               30
                               (send (slot-value 'graph) :num-bins))))
    (when bins 
          (send (slot-value 'graph) :num-bins bins)
          (send (slot-value 'graph) :redraw))))

;;;;
;;;;
;;;; Scatterplot Object Prototype
;;;;
;;;;

(send scatterplot-proto :title "Plot")
(send scatterplot-proto :menu-title "Plot")
(send scatterplot-proto :fixed-aspect nil)
(send scatterplot-proto :menu-template (send graph-proto :menu-template))

(defmeth graph-proto :add-function (f xmin xmax &rest args &key (num-points 50))
"Message args: (f xmin xmax &key (num-points 50)
Adds plot of function F of one real variable over the range between xmin
and xmax to the plot. The function is evaluated at NUM-POINTS points."
  (unless (= 2 (send self :num-variables)) (error "only works for 2D plots"))
  (let* ((x (rseq xmin xmax num-points))
         (y (mapcar f x)))
    (apply #'send self :add-lines (list x y) args)))

(defmeth graph-proto :abline (a b)
"Message args: (a b)
Adds the graph of the line A + B x to the plot."
  (let ((limits (send self :range 0)))
    (send self :add-function #'(lambda (x) (+ a (* b x)))
          (car limits)
          (cadr limits))))

(defmeth graph-proto :plotline (a b c d draw)
  (send self :add-lines (list a c) (list b d) :draw draw))

;;;;
;;;;
;;;; Basic 2D Plotting Functions
;;;;
;;;;

(defun plot-function (f xmin xmax &key (num-points 50) (type 'solid) labels)
"Args: (f xmin xmax &optional (num-points 50) labels)
Plots function F of one real variable over the range between xmin and xmax.
The function is evaluated at NUM-POINTS points. LABELS is a list of axis
labels."
  (let* ((x (rseq xmin xmax num-points))
         (y (mapcar f x)))
    (plot-lines x y :type type :variable-labels labels)))

;;;;
;;;;
;;;; Boxplot  Functions
;;;;
;;;;

(defmeth scatterplot-proto :add-boxplot (y &key (x 1.0) (width 1.0) (draw t))
  (unless (= 2 (send self :num-variables)) (error "only works for 2D plots"))
  (let* ((half-box (* 0.4 width))
         (half-foot (* 0.1 width))
         (fiv (fivnum y))
         (low (select fiv 0))
         (q1 (select fiv 1))
         (med (select fiv 2))
         (q3 (select fiv 3))
         (high (select fiv 4)))
    (send self :plotline (- x half-foot) low  (+ x half-foot) low  nil)
    (send self :plotline (- x half-foot) high (+ x half-foot) high nil)
    (send self :plotline x low x q1   nil)
    (send self :plotline x q3  x high nil)
    (send self :plotline (- x half-box) q1  (+ x half-box) q1  nil)
    (send self :plotline (- x half-box) med (+ x half-box) med nil)
    (send self :plotline (- x half-box) q3  (+ x half-box) q3  nil)
    (send self :plotline (- x half-box) q1  (- x half-box) q3  nil)
    (send self :plotline (+ x half-box) q1  (+ x half-box) q3  nil)))

(defun boxplot (data &key (title "Box Plot"))
"Args: (data &key (title \"Box Plot\"))
DATA is a sequence, a list of sequences or a matrix. Makes a boxplot of the
sequence or a parallel box plot of the sequences in the list or the columns
of the matrix." 
  (let ((p (send scatterplot-proto :new 2 :title title :show nil)))
    (setq data 
          (cond ((matrixp data) (column-list data))
                ((or (not (listp data)) (numberp (car data))) (list data))
                (t data)))
        (let ((range (get-nice-range (min data) (max data) 4)))
          (send p :range 1 (nth 0 range) (nth 1 range))
          (send p :y-axis t nil (nth 2 range)))
    (send p :range 0 0 (1+ (length data)))
    (dotimes (i (length data))
          (send p :add-boxplot (nth i data) :x (1+ i)))
    (send p :show-window)
    p))

(defun boxplot-x (x data &key (title "Box Plot"))
"Args: (x data &key (title \"Box Plot\"))
DATA is a list of sequences or a matrix. X is a sequence with as many
elements as DATA has elements or columns. Makes a parallel box plot
of the sequences in the list or the columns of the matrix vs X." 
  (let ((p (send scatterplot-proto :new 2 :title title :show nil)))
    (setq data 
          (cond ((matrixp data) (column-list data))
                ((or (not (listp data)) (numberp (car data))) (list data))
                (t data)))
        (let ((range (get-nice-range (min data) (max data) 4)))
          (send p :range 1 (nth 0 range) (nth 1 range))
          (send p :y-axis t nil (nth 2 range)))
    (setq x (coerce x 'list))
    (if (/= (length x) (length data)) (error "argument lengths do not match"))
    (let* ((width (min (difference x)))
           (range (get-nice-range (- (min x) width) (+ (max x) width) 4)))
      (send p :range 0 (nth 0 range) (nth 1 range))
      (send p :x-axis t nil (nth 2 range))
      (dotimes (i (length data))
               (send p :add-boxplot (nth i data) :width width :x (nth i x))))
    (send p :show-window)
    p))

;;;;
;;;;
;;;; Quantile and Probability Plot Functions
;;;;
;;;;

(defun quantile-plot (x &key (quantile-function #'normal-quant) 
                        (title "Quantile Plot")
			point-labels
			(variable-labels
			 '("Theoretical Quantiles" "Observed Quantiles")))
"Args: (data &key (quantile-function #'normal-quant) (title \"Quantile Plot\") point-labels)"
  (plot-points (funcall quantile-function
                        (/ (1+ (rank x)) (1+ (length x))))
               x
               :title title 
               :variable-labels variable-labels
               :point-labels point-labels))

(defun probability-plot (x &key (distribution-function #'normal-cdf)
                           (title "Probability Plot") point-labels)
"Args: (data &key (distribution-function #'normal-cdf) (title \"Probability Plot\") point-labels)"
  (let ((p (plot-points (/ (1+ (rank x)) (1+ (length x)))
                                (funcall distribution-function x)
                                :title title
                                :variable-labels '("Theoretical CDF" "Observed CDF")
                                :point-labels point-labels)))
    (send p :x-axis t t 5)
    (send p :y-axis t t 5)
    p))

;;;;
;;;;
;;;; Contour Plotting Methods and Functions
;;;;
;;;;

(defmeth scatterplot-proto :add-surface-contour (x y z v &key (draw t))
  (let ((c (surface-contour x y z v)))
    (dolist (x (split-list c 2))
            (send self :add-lines (transpose x) :draw nil)))
    (if draw (send self :redraw-content)))
    
(defmeth scatterplot-proto :add-surface-contours 
         (x y z &optional v &key (draw t))
  (let ((v (if v 
               (if (numberp v) (list v v) (coerce v 'list))
               (let ((min (min z))
                     (max (max z)))
                 (+ min (* (- max min) '(.2 .4 .6 .8)))))))
    (dolist (v v) (send self :add-surface-contour x y z v :draw nil))
    (if draw (send self :redraw-content))))

(defmeth scatterplot-proto :add-function-contours 
         (f xmin xmax ymin ymax &optional v &key (num-points 6) (draw t))
  (let* ((x (coerce (rseq xmin xmax num-points) 'vector))
         (y (coerce (rseq ymin ymax num-points) 'vector))
         (z (outer-product x y f)))
    (send self :add-surface-contours x y z v :draw draw)))

(defun contour-function (f xmin xmax ymin ymax &rest args
                           &key levels (num-points 6))
"Args: (f xmin xmax ymin ymax &key levels (num-points 6))
Contour plot of function F of two real variables over the range
between [xmin, xmax] x [ymin, ymax]. The function is evaluated at
NUM-POINTS points."
  (let ((plot (apply #'send scatterplot-proto :new 2 :show nil args)))
    (send plot :add-function-contours f xmin xmax ymin ymax 
          levels :num-points num-points :draw nil)
    (send plot :adjust-to-data :draw nil)
    (send plot :new-menu)
    (send plot :show-window)
    plot))

(require "graph3")
